require 'bigdecimal'

class Model
  attr_reader :accounts, :ofx

  def initialize(data_dir)
    @data_dir = data_dir
    @accounts = { "root" => Account.new("root", 0, nil)}
    @posted_tx = []
    @max_tx_id = 0
    @ofx = {}
    @max_ofx_id = 0

    if(File.exists?(@data_dir))
      load_data
    else
      create_data_dir
    end
  end
  
  def account_file
    "#{@data_dir}/accounts.yaml"
  end
  
  # increase_type parameter should be :debit or :credit
  # to indicate which type of ledger entry will make the account's balance
  # increase
  def add_account(account_name, increase_type, parent_account)
    unless @accounts.has_key?(account_name)
      @accounts[account_name] = Account.new(account_name, increase_type, parent_account)
    end
    return @accounts[account_name]
  end
  
  def adjust(account, debit, credit)
    account.journal(:debit, debit) unless debit == 0
    account.journal(:credit, credit) unless credit == 0
  end

  def append_accounts_to_list(account, account_list)
    account_list << {
      :name => account.account_name,
      :parent => (account.parent_account.nil? ? '' : account.parent_account.account_name),
      :increase_type => account.increase_type
    }
    find_subaccounts(account).each{|a| append_accounts_to_list(a, account_list)}
    return account_list
  end

  def create_data_dir
    Dir.mkdir(@data_dir)
    Dir.mkdir(tx_dir)
    Dir.mkdir("#{@data_dir}/ofx")
  end
   
  def [](account_name)
    return find_account(account_name)
  end
  
  def find_account(account_name)
    if account_name.nil?
      return nil
    else
      return @accounts[account_name]
    end
  end
  
  def find_subaccounts(parent)
    subs = []
    @accounts.each_value do |account|
      subs << account if account.parent_account == parent
    end
    return subs
  end
  
  def find_all_subaccounts(parent)
    subs = []
    children = find_subaccounts(parent)
    subs << children
    children.each do |c|
      subs << find_all_subaccounts(c)
    end
    subs.flatten.compact
  end
  
  def find_transactions(account)
    result = []
    @posted_tx.each do |tx|
      tx.entries.each do |e|
        if(account == e[:account])
          result << tx 
          break
        end
      end
    end
    return result
  end
  
  def import_ofx(source_file)
    @ofx[source_file] = Ofx.new(source_file)
    save_ofx(source_file)
    return @ofx[source_file]
  end
    
  def load_accounts
    File.open(account_file, "r") do |file|
      accounts = YAML.load(file)
      accounts.each do |a|
        add_account(a[:name], a[:increase_type], find_account(a[:parent]))
      end
    end
  end
  
  def load_data
    load_accounts
    load_ofx
    load_tx
  end
  
  def load_ofx
    Dir.glob("#{ofx_dir}/*.yaml") do |ofx_yaml|
      ofx = YAML.load(File.read(ofx_yaml))
      @ofx[ofx.path] = ofx
    end
  end
  
  def load_tx
    tx_files.each do |filename|
      File.open(filename, "r") do |file|
        yaml = YAML.load(file)
        tx = Transaction.new(yaml[:date])
        yaml[:entries].each do |e|
          #entries are: [type, account, amount]
          if :credit == e[0]
            tx.add_credit(find_account(e[1]), BigDecimal._load(e[2]))
          else
            tx.add_debit(find_account(e[1]), BigDecimal._load(e[2]))
          end
        end
        post(tx, false)
      end
    end
  end
  
  def ofx_dir
    "#{@data_dir}/ofx"
  end

  # posts transaction to account journals, "general ledger" and saves to file
  def post(tx, save_to_file = true)
    raise "Cannot post unbalanced transaction" unless tx.is_balanced?
    tx.entries.each{|e| e[:account].journal(e[:tx_type], e[:amount]) }
    tx.watch{|account, debit, credit| adjust(account, debit, credit)}
    @posted_tx << tx
    save_tx(tx) if save_to_file
  end
  
  def root_account
    find_account("root")
  end
  
  def save_accounts
    accts = []
    append_accounts_to_list(root_account, accts)
    File.open(account_file, "w") do |file|
      file.puts accts.to_yaml
    end
  end
  
  def save_ofx(source_file)
    File.open("#{ofx_dir}/#{format("%08d", @max_ofx_id)}.yaml", "w") do |file|
      file.puts @ofx[source_file].to_yaml
    end
    @max_ofx_id += 1
  end
  
  def save_tx(tx)
    tx_data = { :date => tx.date }
    tx_data[:entries] = tx.entries.map{|e|
      [
        e[:tx_type],
        e[:account].account_name,
        e[:amount]._dump
      ] 
    } 
    File.open(tx_file(@max_tx_id), "w") do |file|
      file.puts tx_data.to_yaml
    end
    @max_tx_id += 1
  end
  
  def transactions
    @posted_tx
  end
  
  def tx_dir
    "#{@data_dir}/tx"
  end
  
  def tx_file(tx_id)
    "#{tx_dir}/#{format("%08d", tx_id)}.yaml"
  end
  
  def tx_files
    Dir.glob("#{tx_dir}/*.yaml")
  end

end
